from checkers_config import CheckersGameMode as GameMode, CheckersGlobals as Globals
from checkers_config import *
from checkers_board import *
from ctypes import c_ulong
import random

class Controller(object):
    def __init__(self, game_mode, interface_file=None):
        self._game_mode = game_mode
        self._max_search_depth = DEFAULT_SEARCH_DEPTH
        self._max_value = maxint + 1
        self._min_value = - self._max_value
        self._logic_board = CheckersBoard()
        self._best_state = None
        self._max_no_eat_streak = 30
        
        self._heuristic_functions = {1: self._heuristic_value, 2: self._heuristic_value2, 3: self._heuristic_value3}
        self._heuristic_function = self._heuristic_functions[1]
        
        self._algorithms = [DEFAULT_ALGORITHM, 'minimax']
        self._chosen_algorithm = self._algorithms[0]
        
        self._no_eat_streak = 0
        
        if game_mode == GameMode.VERSUS_EXTERNAL_PROGRAM and interface_file:
            self._interface_file = interface_file
            info = load_info_from_file(interface_file)
        elif game_mode == GameMode.HUMAN_VERSUS_AI:
            self._current_turn = random.choice([CheckersTurns.WHITE, CheckersTurns.BLACK])
            # Human is always white
            self._ai_color = CheckersTurns.BLACK
        elif game_mode == GameMode.AI_VERSUS_AI:
            self._current_turn = random.choice([CheckersTurns.WHITE, CheckersTurns.BLACK])
            self._ai_color = [CheckersTurns.WHITE, CheckersTurns.BLACK]

            
    def set_max_depth(self, max_depth):
        self._max_search_depth = max_depth
        
    def set_heuristic_function(self, heuristic_function_id):
        self._heuristic_function = self._heuristic_functions[heuristic_function_id]
        
    def set_algorithm(self, algorithm_descriptor):
        if algorithm_descriptor == self._chosen_algorithm:
            return
        elif algorithm_descriptor in self._algorithms:
            self._chosen_algorithm = algorithm_descriptor
        else:
            raise ValueError('Algorithm doesn\'t exist')
            
    def get_current_turn(self):
        return CheckersTurns.to_string(self._current_turn)
        
    def _max_turns_with_no_jumps(self):
        if self._no_eat_streak >= self._max_no_eat_streak:
            return True
        else:
            return False
        
    def is_game_over(self):
        if self._logic_board.cwhites.value == 0:
            return CheckersEnd.BLACK_WON
        elif self._logic_board.cblacks.value == 0:
            return CheckersEnd.WHITE_WON
        elif not self._get_possibilities(self._logic_board, turn=self._current_turn):
            return CheckersEnd.DRAW
        elif self._max_turns_with_no_jumps():
            return CheckersEnd.DRAW
        else:
            return False
    
    def _get_new_locations_for_human_piece(self, piece):
        new_locations = c_ulong(0) #ZERO_BITARRAY.copy()
        new_boards = self._get_possibilities(self._logic_board, turn=CheckersTurns.WHITE)
        for new_board in new_boards:
            if (new_board.cwhites.value & piece) == 0:
                new_locations.value |= flipnum(self._logic_board.cwhites.value) & new_board.cwhites.value
        return new_locations.value
    
    def get_opponent_pieces(self):
        if self._current_turn == CheckersTurns.WHITE:
            return self._logic_board.cblacks.value
        else:
            return self._logic_board.cwhites.value
    
    def _move_location_for_human_piece(self, old_location, new_location):
        new_boards = self._get_possibilities(self._logic_board, turn=CheckersTurns.WHITE)
        for new_board in new_boards:
            if ((new_board.cwhites.value & old_location) == 0) and (new_board.cwhites.value & new_location):
                before = self.get_opponent_pieces()
                self._logic_board.copy_board_pieces(new_board)
                if before != self.get_opponent_pieces():
                    self._no_eat_streak = 0
                else:
                    self._no_eat_streak += 1
                break
        
    def AI_play(self):
        if self._chosen_algorithm == 'minimax':
            minimax_score = self.minimax(self._logic_board, 0)
        elif self._chosen_algorithm == 'alpha-beta':
            alpha_beta_score = self.alpha_beta(self._logic_board, 0, self._min_value, self._max_value)
        else:
            raise ValueError('Chosen algorithm unknown')
        before = self.get_opponent_pieces()
        self._logic_board.copy_board_pieces(self._best_state)
        if before != self.get_opponent_pieces():
            self._no_eat_streak = 0
        else:
            self._no_eat_streak += 1

    def minimax(self, board, current_depth):
        if current_depth >= self._max_search_depth:
            return self._heuristic_function(board)
        is_white = (self._current_turn == CheckersTurns.WHITE)
        is_empty_wp, is_empty_bp = board.cwhites.value == 0, board.cblacks.value == 0
        if (is_white and is_empty_wp) or ((not is_white) and is_empty_bp):
            return self._min_value;
        elif (is_white and is_empty_bp) or ((not is_white) and is_empty_wp):
            return self._max_value;
            
        odd = current_depth & 1
        # White for even depths, black for odd
        is_white = is_white ^ odd
        moves = self._get_possibilities(board, CheckersTurns.WHITE) if is_white else self._get_possibilities(board, CheckersTurns.BLACK)
        
        if not moves:
            if current_depth == 0:
                self._best_state = None
            return self._heuristic_function(board)
          
        best_value, new_value, cmp_result = 0, 0, 0
        if odd:
            best_value, cmp_result = self._max_value, -1
        else:
            best_value, cmp_result = self._min_value, 1

        for index in xrange(len(moves)):
            new_value = self.minimax(moves[index], current_depth + 1)
            if cmp(new_value, best_value) == cmp_result:
                best_value = new_value
                if current_depth == 0:
                    self._best_state = moves[index]
            
        return best_value

    def alpha_beta(self, board, current_depth, alpha, beta):
        is_empty_wp, is_empty_bp = board.cwhites.value == 0, board.cblacks.value == 0

        if current_depth >= self._max_search_depth or is_empty_wp or is_empty_bp:
            return self._heuristic_function(board)

        is_white = (self._current_turn == CheckersTurns.WHITE)
        if (is_white and is_empty_wp) or ((not is_white) and is_empty_bp):
            return self._min_value;
        elif (is_white and is_empty_bp) or ((not is_white) and is_empty_wp):
            return self._max_value;

        odd = current_depth & 1
        # White for even depths, black for odd
        is_white = is_white ^ odd
        
        moves = self._get_possibilities(board, CheckersTurns.WHITE) if is_white else self._get_possibilities(board, CheckersTurns.BLACK)
         
        if not moves:
            if current_depth == 0:
                self._best_state = None
            return self._heuristic_function(board)
            
        if not odd:
            for index in xrange(len(moves)):
                best_value = self.alpha_beta(moves[index], current_depth + 1, alpha, beta)
                if best_value > alpha:
                    alpha = best_value
                    if current_depth == 0:
                        self._best_state = moves[index]
                if alpha >= beta - 1:
                    return alpha
            return alpha
        else:
            for index in xrange(len(moves)):
                best_value = self.alpha_beta(moves[index], current_depth + 1, alpha, beta)
                beta = min(beta, best_value)
                if beta <= alpha + 1:
                    return beta
            return beta
        
    def _get_movable_pieces(self, board=None, directions=None, turn=None):
        if turn is None:
            turn = self._current_turn
        if board is None:
            board = self._logic_board
        if directions is None:
            directions = DIRECTIONS._fields
        
        movable_pieces, relevant_north, relevant_south = c_ulong(0), c_ulong(0), c_ulong(0)
        empty_places = c_ulong(flipnum(board.cblacks.value | board.cwhites.value))
        
        if turn == CheckersTurns.BLACK:
            relevant_north.value = board.cblacks.value & board.ckings.value
            relevant_south.value = board.cblacks.value
        else:
            relevant_north.value = board.cwhites.value
            relevant_south.value = board.cwhites.value & board.ckings.value
        
        if 'SE' in directions:
            movable_pieces.value |= ((empty_places.value & 0x00F0F0F0) << 4) & relevant_south.value
            movable_pieces.value |= ((empty_places.value & 0x0E0E0E0E) << 3) & relevant_south.value
        if 'NE' in directions:
            movable_pieces.value |= ((empty_places.value & 0xF0F0F0F0) >> 4) & relevant_north.value
            movable_pieces.value |= ((empty_places.value & 0x0E0E0E00) >> 5) & relevant_north.value
        if 'SW' in directions:
            movable_pieces.value |= ((empty_places.value & 0x0F0F0F0F) << 4) & relevant_south.value
            movable_pieces.value |= ((empty_places.value & 0x00707070) << 5) & relevant_south.value
        if 'NW' in directions:
            movable_pieces.value |= ((empty_places.value & 0x0F0F0F0F) >> 4) & relevant_north.value
            movable_pieces.value |= ((empty_places.value & 0x70707070) >> 3) & relevant_north.value

        return movable_pieces.value
        
    def _get_south_jump(self, empties, mask, cond_shift, res_shift, eaters, eatable):
        value = ((empties & mask) << cond_shift) & eatable
        if value:
            return ((value << res_shift) & eaters)
        else:
            return 0

    def _get_north_jump(self, empties, mask, cond_shift, res_shift, eaters, eatable):
        value = ((empties & mask) >> cond_shift) & eatable
        if value:
            return ((value >> res_shift) & eaters)
        else:
            return 0
            
    def _get_jumpable_pieces(self, board=None, directions=None, turn=None):
        if turn is None:
            turn = self._current_turn
        if board is None:
            board = self._logic_board
        if directions is None:
            directions = DIRECTIONS._fields
        
        jumpable_pieces, relevant_eatable,  relevant_north_eaters, relevant_south_eaters = c_ulong(0), c_ulong(0), c_ulong(0), c_ulong(0)
        empty_places = c_ulong(flipnum(board.cblacks.value | board.cwhites.value))
        
        if turn == CheckersTurns.BLACK:
            relevant_eatable.value = board.cwhites.value
            relevant_north_eaters.value = board.cblacks.value & board.ckings.value
            relevant_south_eaters.value = board.cblacks.value
        else:
            relevant_eatable.value = board.cblacks.value
            relevant_north_eaters.value = board.cwhites.value
            relevant_south_eaters.value = board.cwhites.value & board.ckings.value
        
        if 'SE' in directions:
            jumpable_pieces.value |= self._get_south_jump(empty_places.value, 0x00E0E0E0, 4, 3, relevant_south_eaters.value, relevant_eatable.value)
            jumpable_pieces.value |= self._get_south_jump(empty_places.value, 0x000E0E0E, 3, 4, relevant_south_eaters.value, relevant_eatable.value)
        if 'NE' in directions:
            jumpable_pieces.value |= self._get_north_jump(empty_places.value, 0xE0E0E000, 4, 5, relevant_north_eaters.value, relevant_eatable.value)
            jumpable_pieces.value |= self._get_north_jump(empty_places.value, 0x0E0E0E00, 5, 4, relevant_north_eaters.value, relevant_eatable.value)  
        if 'SW' in directions:
            jumpable_pieces.value |= self._get_south_jump(empty_places.value, 0x00070707, 4, 5, relevant_south_eaters.value, relevant_eatable.value)
            jumpable_pieces.value |= self._get_south_jump(empty_places.value, 0x00707070, 5, 4, relevant_south_eaters.value, relevant_eatable.value)
        if 'NW' in directions:
            jumpable_pieces.value |= self._get_north_jump(empty_places.value, 0x07070700, 4, 3, relevant_north_eaters.value, relevant_eatable.value)
            jumpable_pieces.value |= self._get_north_jump(empty_places.value, 0x70707000, 3, 4, relevant_north_eaters.value, relevant_eatable.value)

        return jumpable_pieces.value
    
    def _get_south_move(self, counter, mask, shift):
        if counter & mask:
            return counter >> shift
        return None
            
    def _get_north_move(self, counter, mask, shift):
        if counter & mask:
            return counter << shift
        else:
            return None
    
    def _get_available_moves(self, board, direction, turn=None):
        if turn is None:
            turn = self._current_turn
            
        moves = []
        mapp = moves.append
        index, counter = 0, c_ulong(1)
        
        movable = c_ulong(self._get_movable_pieces(board=board, directions=[direction], turn=turn))
        if not movable.value:
            return moves
            
        moves_board = CheckersBoard(board)
            
        for index in xrange(Globals.AVAILABLE_SQUARES):
        
            if turn == CheckersTurns.BLACK:
                set_pieces = moves_board.set_bp
                relevant_pieces = moves_board.cblacks.value
            else:
                set_pieces = moves_board.set_wp
                relevant_pieces = moves_board.cwhites.value
            
            if movable.value & counter.value:
                or_value = None
                if direction == 'SE':
                    if counter.value & 0x0F0F0F00:
                        or_value = counter.value >> 4
                    elif counter.value & 0x70707070:
                        or_value = counter.value >> 3
                elif direction == 'SW':
                    if counter.value & 0xF0F0F0F0:
                        or_value = counter.value >> 4
                    elif counter.value & 0x0E0E0E00:
                        or_value = counter.value >> 5
                elif direction == 'NE':
                    if counter.value & 0x0F0F0F0F:
                        or_value = counter.value << 4
                    elif counter.value & 0x00707070:
                        or_value = counter.value << 5
                elif direction == 'NW':
                    if counter.value & 0x00F0F0F0:
                        or_value = counter.value << 4
                    elif counter.value & 0x0E0E0E0E:
                        or_value = counter.value << 3
                
                if or_value is not None:
                    or_value = c_ulong(or_value)
                    # Delete & set new piece
                    after_move = ((relevant_pieces & flipnum(counter.value)) | or_value.value)
                    set_pieces(after_move)
                    # If King
                    if counter.value & board.ckings.value:
                        moves_board.ckings.value = ((moves_board.ckings.value & flipnum(counter.value)) | or_value.value)

                    #moves.append(CheckersBoard(moves_board))
                    mapp(moves_board)
                    moves_board = CheckersBoard(self._logic_board)

            counter.value = counter.value << 1
            
        return moves

    def _get_available_jumps(self, board, direction, turn=None):
        if turn is None:
            turn = self._current_turn
            
        jumps_boards = []
        jb_append = jumps_boards.append
        index, counter = 0, c_ulong(1)
        jumpable = c_ulong(self._get_jumpable_pieces(board=board, directions=[direction], turn=turn))
        jumps_board = CheckersBoard(board) 
        
        for index in xrange(Globals.AVAILABLE_SQUARES):
            if turn == CheckersTurns.BLACK:
                set_pieces = jumps_board.set_bp
                relevant_pieces = jumps_board.cblacks.value
                opponent_pieces = jumps_board.cwhites.value
                set_op_pieces = jumps_board.set_wp
            else:
                set_pieces = jumps_board.set_wp
                relevant_pieces = jumps_board.cwhites.value
                opponent_pieces = jumps_board.cblacks.value
                set_op_pieces = jumps_board.set_bp
            
            if jumpable.value & counter.value:
                or_value = c_ulong(0)
                del_value = c_ulong(0)
                # Prepare masks
                if direction == 'SE':
                    or_value.value = counter.value >> 7
                    if counter.value & 0x07070700:
                        del_value.value = counter.value >> 4
                    if counter.value & 0x70707000:
                        del_value.value = counter.value >> 3
                        
                elif direction == 'SW':
                    or_value.value = counter.value >> 9
                    if counter.value & 0xE0E0E000:
                        del_value.value = counter.value >> 4
                    if counter.value & 0x0E0E0E00:
                        del_value.value = counter.value >> 5
                        
                elif direction == 'NE':
                    or_value.value = counter.value << 9
                    if counter.value & 0x00070707:
                        del_value.value = counter.value << 4
                    if counter.value & 0x00707070:
                        del_value.value = counter.value << 5
                        
                elif direction == 'NW':
                    or_value.value = counter.value << 7
                    if counter.value & 0x00E0E0E0:
                        del_value.value = counter.value << 4
                    if counter.value & 0x000E0E0E:
                        del_value.value = counter.value << 3
                        
                # Delete old piece & set new one
                relevant_pieces = ((relevant_pieces & flipnum(counter.value)) | or_value.value)
                set_pieces(relevant_pieces)
                # Check if king
                if counter.value & board.ckings.value:
                    jumps_board.ckings.value = ((jumps_board.ckings.value & flipnum(counter.value)) | or_value.value)
                # Remove eaten pieces
                if del_value.value:
                    opponent_pieces = opponent_pieces & flipnum(del_value.value)
                    set_op_pieces(opponent_pieces)
                    # Check if king
                    if del_value.value & jumps_board.ckings.value:
                        jumps_board.ckings.value = jumps_board.ckings.value & flipnum(del_value.value)
                    
                # Check for additional jumps    
                if self._get_jumpable_pieces(board=jumps_board, turn=turn) & or_value.value:
                    additional_jumps = []
                    aj_extend = additional_jumps.extend
                    aj_extend(self._get_available_jumps(jumps_board, 'SE', turn=turn))
                    aj_extend(self._get_available_jumps(jumps_board, 'NE', turn=turn))
                    aj_extend(self._get_available_jumps(jumps_board, 'SW', turn=turn))
                    aj_extend(self._get_available_jumps(jumps_board, 'NW', turn=turn))
                    for additional_jump in additional_jumps:
                        empty = flipnum(additional_jump.cblacks.value | additional_jump.cwhites.value)
                        if empty & or_value.value:
                            jb_append(additional_jump)
                else:
                    #jb_append(CheckersBoard(jumps_board))
                    jb_append(jumps_board)
                
                jumps_board = CheckersBoard(board) 
            
            counter.value = counter.value << 1
            
        return jumps_boards
     
    def _get_possibilities(self, board, turn=None):
        if turn is None:
            turn = self._current_turn
        moves = []
        extend = moves.extend
        if self._get_jumpable_pieces(board=board, directions=DIRECTIONS._fields, turn=turn):
            extend(self._get_available_jumps(board, 'SE', turn=turn))
            extend(self._get_available_jumps(board, 'NE', turn=turn))
            extend(self._get_available_jumps(board, 'SW', turn=turn))
            extend(self._get_available_jumps(board, 'NW', turn=turn))
        else:
            extend(self._get_available_moves(board, 'SE', turn=turn))
            extend(self._get_available_moves(board, 'NE', turn=turn))
            extend(self._get_available_moves(board, 'SW', turn=turn))
            extend(self._get_available_moves(board, 'NW', turn=turn))
        return moves
        
        
    def _heuristic_value(self, board):
        white_kings, black_kings = board.cwhites.value & board.ckings.value, board.cblacks.value & board.ckings.value
        white_regulars, black_regulars = board.cwhites.value ^ white_kings, board.cblacks.value ^ black_kings
        white_value, black_value = 0, 0
        
        white_movable = c_ulong(self._get_movable_pieces(board=board, turn=CheckersTurns.WHITE))
        black_movable = c_ulong(self._get_movable_pieces(board=board, turn=CheckersTurns.BLACK))
        white_movable_kings, black_movable_kings = white_movable.value & board.ckings.value, black_movable.value & board.ckings.value
        white_movable_regulars, black_movable_regulars =  white_movable_kings ^ white_movable.value, black_movable_kings ^ black_movable.value
         
        white_value += 10 * bit_count(white_regulars)
        white_value += 15 * bit_count(white_kings)
        white_value += 10 * bit_count(white_movable_regulars)
        white_value += 15 * bit_count(white_movable_kings)
        white_value += 10 * bit_count(white_regulars & 0xFFFFF000)
        white_value += 5 * bit_count(white_regulars & 0xFF000000)
        white_value -= bit_count(white_movable_regulars & (0x00000FFF))
        
        black_value += 10 * bit_count(black_regulars)
        black_value += 15 * bit_count(black_kings)
        black_value += 10 * bit_count(black_movable_regulars)
        black_value += 15 * bit_count(black_movable_kings)
        black_value += 10 * bit_count(black_regulars & 0x000FFFFF)
        black_value += 5 * bit_count(black_regulars & 0x000000FF)
        black_value -= bit_count(black_movable_regulars & (0xFFF00000))

        return (white_value - black_value) if self._current_turn == CheckersTurns.WHITE else (black_value - white_value)
      
    def _heuristic_value2(self, board):
        whites = board.cwhites.value
        blacks = board.cblacks.value
        white_kings, black_kings = whites & board.ckings.value, blacks & board.ckings.value
        white_regulars, black_regulars = whites ^ white_kings, blacks ^ black_kings
        white_value, black_value = 0, 0
        
        total = blacks | whites
        
        white_value += 5 * bit_count(white_regulars & 0x0000FFFF)
        white_value += 7 * bit_count(white_regulars & 0xFFFF0000)
        white_value += 10 * bit_count(white_kings)
        white_value /= bit_count(total)
        
        black_value += 5 * bit_count(black_regulars & 0x0000FFFF)
        black_value += 7 * bit_count(black_regulars & 0xFFFF0000)
        black_value += 10 * bit_count(black_kings)
        black_value /= bit_count(total)
        
        return (white_value - black_value) if self._current_turn == CheckersTurns.WHITE else (black_value - white_value)
      
 
    def _heuristic_value3(self, board):
        whites = board.cwhites.value
        blacks = board.cblacks.value
        white_kings, black_kings = whites & board.ckings.value, blacks & board.ckings.value
        white_regulars, black_regulars = whites ^ white_kings, blacks ^ black_kings
        white_value, black_value = 0, 0
        
        total = blacks | whites
        
        white_value += 5 * bit_count(white_regulars)
        white_value += 10 * bit_count(white_kings)
        black_value -= bit_count(total)
        
        black_value += 5 * bit_count(black_regulars)
        black_value += 10 * bit_count(black_kings)
        black_value -= bit_count(total)
        
        return (white_value - black_value) if self._current_turn == CheckersTurns.WHITE else (black_value - white_value)
      

        
        
        
  